﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO;
using System.Xml.Serialization;
using System.Xml.Linq;
using System.Threading;

using log4net;
using NHibernate;
using Pink.Scheduler.DataAccess;

namespace Pink.Scheduler.RemoteInterfaces
{
	/// <summary>
	/// Класс, который загружает задачи с диска в БД в реальном времени
	/// </summary>
	public class TaskImporter
	{
		private ILog logger;
		private Timer recreator;
		private bool listChanged = false;
		private List<FileSystemWatcher> watchers = new List<FileSystemWatcher>();
		private TimeSpan recreatorInterval = TimeSpan.FromSeconds(5);
		/// <summary>
		/// Файл импортов (список пресетов и инструкции)
		/// </summary>
		string importsListFile;
		/// <summary>
		/// Список импортов, файл импортов игнорируется
		/// </summary>
		string[] overrideImports;
		private ISessionFactory factory;

		/// <summary>
		/// Создать и запустить загружатель задач, загружающий список импортов и инструкций из файла
		/// </summary>
		/// <param name="importsFile">Из какого файла грузить список импортов и инструкций</param>
		/// <param name="factory">Фабрика сессий NHibernate</param>
		public TaskImporter(string importsFile, ISessionFactory factory) : this(factory)
		{
			this.importsListFile = importsFile;
			Start();
		}

		/// <summary>
		/// Создать и запустить загружатель задач, загружающий указанные импорты
		/// </summary>
		/// <param name="imports">Явный список импортов</param>
		/// <param name="factory">Фабрика сессий NHibernate</param>
		public TaskImporter(string[] imports, ISessionFactory factory) : this(factory)
		{
			this.overrideImports = imports;
			Start();
		}

		private TaskImporter(ISessionFactory factory)
		{
			logger = LogManager.GetLogger(GetType());
			this.factory = factory;
		}

		private void Start()
		{
			ReloadTasks();
			recreator = new Timer(TimerCallback, null, TimeSpan.FromSeconds(0), recreatorInterval);
		}
		/// <summary>
		/// Остановить загрузку и обновление задач
		/// </summary>
		public void Stop()
		{
			// после убийства таймера события обрабатываться не будут
			recreator.Dispose();
			watchers.ForEach(a => a.Dispose());
		}

		/// <summary>
		/// Перезагрузить список задач и пересоздать вотчеров
		/// </summary>
		/// <returns>true если перезагрузка что-либо изменила в БД</returns>
		private bool ReloadTasks()
		{
			bool result = false;
			string[] imports = overrideImports;
			List<string> ignore, enable, disable;
			// или смотрим список в массиве или грузим из файла
			if (imports != null)
				ignore = enable = disable = new List<string>();
			else
			{
				imports = LoadConfig(importsListFile, out ignore, out enable, out disable);
				watchers.Add(CreateWatcher(importsListFile));
			}

			Dictionary<string, TaskData> importTasks = new Dictionary<string, TaskData>();
			foreach (string file in imports)
			{
				TaskData[] current = LoadTasks(file);
				logger.InfoFormat("Загружен файл {0}, задачи [{1}]", file,
					string.Join(", ", current.Select(a => a.ID).ToArray()));
				watchers.Add(CreateWatcher(file));
				foreach (TaskData task in current)
				{
					// ключи задач должны быть уникальны
					if (importTasks.ContainsKey(task.ID))
						throw new InvalidOperationException(string.Format("Задача {0} уже загружена! Ключи задач должны быть уникальны.", task.ID));
					// список игнорирования
					if (ignore.Contains(task.ID))
					{
						logger.InfoFormat("Задача {0} пропущена при загрузке из файла (список ignore).", task.ID);
						continue;
					}
					else
						importTasks.Add(task.ID, task);
				}
			}

			using (ISession session = factory.OpenSession())
			{
				Dictionary<string, TaskData> databaseTasks = session.CreateCriteria(typeof(TaskData))
					.List<TaskData>()
					.ToDictionary(a => a.ID, a => a);
				string[] ignored = databaseTasks.Keys.Intersect(ignore).ToArray();
				// обработка списков enable и disable в базе данных
				foreach (string id in databaseTasks.Keys)
				{
					bool changed = false;
					if (enable.Contains(id) && !databaseTasks[id].IsEnabled)
					{
						databaseTasks[id].IsEnabled = true;
						changed = true;
						logger.InfoFormat("Задача {0} включена (список enable).", id);
					}
					if (disable.Contains(id) && databaseTasks[id].IsEnabled)
					{
						databaseTasks[id].IsEnabled = false;
						changed = true;
						logger.InfoFormat("Задача {0} выключена (список disable).", id);
					}
					if (changed)
					{
						session.Update(databaseTasks[id]);
						session.Flush();
					}
				}
				if (ignored.Length > 0)
				{
					logger.InfoFormat("Задачи [{0}] останутся в БД неизменными (список ignore).",
						string.Join(", ", ignored));
					foreach (string ignoredID in ignored)
						databaseTasks.Remove(ignoredID);
				}
				// выключим задачи, которые есть в БД, но нет в списке добавленных или включённых
				foreach (string databaseID in databaseTasks.Keys)
				{
					if (!importTasks.ContainsKey(databaseID) &&
						databaseTasks[databaseID].IsEnabled
						&& !enable.Contains(databaseID))
					{
						logger.InfoFormat("Задача {0} найдена в БД, но отсутствует в списке импортов и поэтому выключена.", databaseID);
						databaseTasks[databaseID].IsEnabled = false;
						session.Update(databaseTasks[databaseID]);
						session.Flush();
						result = true;
					}
				}
				// добавим задачи, которые есть в списке, но нет в БД и в списке выключенных
				foreach (string importID in importTasks.Keys)
				{
					if (!databaseTasks.ContainsKey(importID) && !disable.Contains(importID))
					{
						logger.InfoFormat("Задача {0} добавлена в БД.", importID);
						session.Save(importTasks[importID]);
						session.Flush();
						result = true;
					}
				}
				// обновим задачи, которые есть и там и там, но могли измениться
				foreach (string id in importTasks.Keys.Intersect(databaseTasks.Keys))
				{
					TaskData o = databaseTasks[id];
					TaskData n = importTasks[id];
					bool changed = false;
					changed |= o.Assembly != n.Assembly;
					changed |= o.Config != n.Config;
					changed |= o.IsEnabled != n.IsEnabled;
					changed |= o.Name != n.Name;
					changed |= o.RetryDelaySeconds != n.RetryDelaySeconds;
					changed |= o.RetryWithinSeconds != n.RetryWithinSeconds;
					changed |= o.ScheduleDay != n.ScheduleDay;
					changed |= o.ScheduleTimeSeconds != n.ScheduleTimeSeconds;
					changed |= o.ScheduleType != n.ScheduleType;
					changed |= o.TaskClassName != n.TaskClassName;
					if (changed)
					{
						// подменим объект новой копией
						session.Evict(databaseTasks[id]);
						n = (TaskData)session.SaveOrUpdateCopy(n);
						logger.InfoFormat("Параметры задачи {0} изменились.", id);
						session.Update(n);
						session.Flush();
						result = true;
					}
				}
			}
			// запустим наших вотчеров
			foreach (FileSystemWatcher w in watchers)
				w.EnableRaisingEvents = true;
			return result;
		}
		/// <summary>
		/// Создать неактивного смотрителя за файлом
		/// </summary>
		/// <param name="file"></param>
		/// <returns></returns>
		private FileSystemWatcher CreateWatcher(string file)
		{
			string fullPath = Path.GetFullPath(file);
			FileSystemWatcher watcher = new FileSystemWatcher(Path.GetDirectoryName(fullPath),
				Path.GetFileName(file));
			watcher.NotifyFilter = NotifyFilters.LastWrite;
			watcher.Changed += new FileSystemEventHandler(watcher_Changed);
			return watcher;
		}

		private void watcher_Changed(object sender, FileSystemEventArgs e)
		{
			logger.DebugFormat("Изменился файл {0}.", e.Name);
			string fileFolder = Path.GetDirectoryName(e.FullPath);
			// это чтобы не сразиться с таймером
			lock (watchers)
			{
				if (watchers.Count == 0)
					return;
				// удаляем старые вотчеры и пересоздадим их чтобы не геморроиться 
				// с синхронизацией списка в случае изменения набора файлов
				foreach (FileSystemWatcher watcher in watchers)
					watcher.Dispose();
				watchers.Clear();
				listChanged = true;
				// дёрнуть таймер, чтобы не ждать лишнего времени
				recreator.Change(TimeSpan.FromSeconds(0), recreatorInterval);
			}
		}

		private void TimerCallback(object o)
		{
			if (listChanged)
			{
				// это чтобы не сражаться с изменениями вотчеров и с самим собой
				lock (watchers)
				{
					try
					{
						if (listChanged)
						{
							bool dbChanged = ReloadTasks();
							listChanged = false;
							if (dbChanged && EventSubscriber != null)
								EventSubscriber.NotifyTasksUpdated();
						}
					}
					catch (Exception ex)
					{
						logger.Error("Ошибка перезагрузки списка задач с диска!", ex);
					}
				}
			}
		}

		/// <summary>
		/// Загрузить список импортов и игноров
		/// </summary>
		/// <param name="file">Из какого файла грузить</param>
		/// <param name="ignore">Список задач, которые не грузятся из файлов и не обновляются в БД</param>
		/// <param name="disable">Список задач, которые нужно выключить в базе и не грузить из файлов</param>
		/// <param name="enable">Список задач, которые нужно включить в базе и не отключать если их нет в файлах</param>
		/// <returns></returns>
		private string[] LoadConfig(string file,
			out List<string> ignore, out List<string> enable, out List<string> disable)
		{
			using (StreamReader sr = new StreamReader(file))
			{
				//получаем коллекцию настроек из файла
				XDocument xml = XDocument.Load(sr);
				string[] result = (
					from item in xml.Root.Elements("import")
					select item.Attribute("file").Value).ToArray();
				ignore = (from item in xml.Root.Elements("ignore")
						  select item.Attribute("task").Value).ToList();
				enable = (from item in xml.Root.Elements("enable")
						  select item.Attribute("task").Value).ToList();
				disable = (from item in xml.Root.Elements("disable") 
						   select item.Attribute("task").Value).ToList();

				return result;
			}
		}

		/// <summary>
		/// Загрузить задачи из файла
		/// </summary>
		/// <param name="file">Файл со списком задач</param>
		/// <returns></returns>
		private TaskData[] LoadTasks(string file)
		{
			string fileDir = Path.GetDirectoryName(Path.GetFullPath(file));
			XmlAttributeOverrides overrides = new XmlAttributeOverrides();
			XmlAttributes attribs = new XmlAttributes { XmlIgnore = true };
			overrides.Add(typeof(TaskData), TaskData.ScheduleTimeSecondsProp, attribs);
			XmlSerializer xs = new XmlSerializer(typeof(TaskData[]), overrides);
			using (StreamReader sr = new StreamReader(file))
			{
				TaskData[] result = (TaskData[])xs.Deserialize(sr);
				foreach (TaskData task in result)
				{
					// пересчитать относительные пути к задаче от файла со списком
					if (!Path.IsPathRooted(task.Assembly))
						task.Assembly = Path.Combine(fileDir, task.Assembly);
				}
				return result;
			}
		}
		
		/// <summary>
		/// Диспетчер, который подписывается на событие изменения списка задач
		/// </summary>
		public TaskDispatcher EventSubscriber { private get; set; }
	}
}
